/**
 * Copyright 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package net.oauth.jsontoken;

import java.security.SignatureException;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.commons.codec.binary.Base64;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import net.oauth.jsontoken.crypto.AsciiStringVerifier;
import net.oauth.jsontoken.crypto.SignatureAlgorithm;
import net.oauth.jsontoken.crypto.Verifier;
import net.oauth.jsontoken.discovery.VerifierProviders;

/**
 * Created by steve on 12/09/14.
 */
public class JsonTokenParser {
    private final Clock clock;
    private final VerifierProviders verifierProviders;
    private final Checker[] checkers;
    ObjectMapper mapper = new ObjectMapper();

    /**
     * Creates a new {@link JsonTokenParser} with a default system clock. The default
     * system clock tolerates a clock skew of up to {@link SystemClock#DEFAULT_ACCEPTABLE_CLOCK_SKEW_IN_MIN}.
     *
     * @param verifierProviders an object that provides signature verifiers
     *   based on a signature algorithm, the signer, and key ids.
     * @param checker an audience checker that validates the audience in the JSON token.
     */
    public JsonTokenParser(VerifierProviders verifierProviders, Checker checker) {
        this(new SystemClock(), verifierProviders, checker);
    }

    /**
     * Creates a new {@link JsonTokenParser}.
     *
     * @param clock a clock object that will decide whether a given token is
     *   currently valid or not.
     * @param verifierProviders an object that provides signature verifiers
     *   based on a signature algorithm, the signer, and key ids.
     * @param checkers an array of checkers that validates the parameters in the JSON token.
     */
    public JsonTokenParser(Clock clock, VerifierProviders verifierProviders, Checker... checkers) {
        this.clock = JsonTokenUtil.checkNotNull(clock);
        this.verifierProviders = verifierProviders;
        this.checkers = checkers;
    }

    /**
     * Decodes the JWT token string into a JsonToken object. Does not perform
     * any validation of headers or claims.
     *
     * @param tokenString The original encoded representation of a JWT
     * @return Unverified contents of the JWT as a JsonToken
     */
    public JsonToken deserialize(String tokenString) throws Exception {
        String[] pieces = splitTokenString(tokenString);
        String jwtHeaderSegment = pieces[0];
        String jwtPayloadSegment = pieces[1];
        byte[] signature = Base64.decodeBase64(pieces[2]);
        Map<String, Object> header = mapper.readValue(JsonTokenUtil.fromBase64ToJsonString(jwtHeaderSegment),
                new TypeReference<LinkedHashMap<String, Object>>() {});
        Map<String, Object> payload = mapper.readValue(JsonTokenUtil.fromBase64ToJsonString(jwtPayloadSegment),
                new TypeReference<LinkedHashMap<String, Object>>() {});
        JsonToken jsonToken = new JsonToken(header, payload, clock, tokenString);
        return jsonToken;
    }

    /**
     * Verifies that the jsonToken has a valid signature and valid standard claims
     * (iat, exp). Uses VerifierProviders to obtain the secret key.
     *
     * @param jsonToken
     * @throws java.security.SignatureException
     */
    public void verify(JsonToken jsonToken) throws SignatureException {
        List<Verifier> verifiers = provideVerifiers(jsonToken);
        verify(jsonToken, verifiers);
    }

    /**
     * Parses, and verifies, a JSON Token.
     *
     * @param tokenString the serialized token that is to parsed and verified.
     * @return the deserialized {@link JsonToken}, suitable for passing to the constructor
     *   of {@link JsonToken} or equivalent constructor of {@link JsonToken} subclasses.
     * @throws SignatureException
     */
    public JsonToken verifyAndDeserialize(String tokenString) throws Exception {
        JsonToken jsonToken = deserialize(tokenString);
        verify(jsonToken);
        return jsonToken;
    }

    /**
     * Verifies that the jsonToken has a valid signature and valid standard claims
     * (iat, exp). Does not need VerifierProviders because verifiers are passed in
     * directly.
     *
     * @param jsonToken the token to verify
     * @throws SignatureException when the signature is invalid
     * @throws IllegalStateException when exp or iat are invalid
     */
    public void verify(JsonToken jsonToken, List<Verifier> verifiers) throws SignatureException {
        if (!signatureIsValid(jsonToken.getTokenString(), verifiers)) {
            throw new SignatureException("Invalid signature for token: " + jsonToken.getTokenString());
        }

        Instant issuedAt = jsonToken.getIssuedAt();
        Instant expiration = jsonToken.getExpiration();

        if (issuedAt == null && expiration != null) {
            issuedAt = Instant.ofEpochMilli(0);
        }

        if (issuedAt != null && expiration == null) {
            expiration = Instant.ofEpochMilli(Long.MAX_VALUE);
        }

        if (issuedAt != null && expiration != null) {
            if (issuedAt.isAfter(expiration) || !clock.isCurrentTimeInInterval(issuedAt, expiration)) {
                throw new IllegalStateException(String.format("Invalid iat and/or exp. iat: %s exp: %s " + "now: %s",
                        jsonToken.getIssuedAt(), jsonToken.getExpiration(), clock.now()));
            }
        }

        if (checkers != null) {
            for (Checker checker : checkers) {
                checker.check(jsonToken.getPayload());
            }
        }
    }

    /**
     * Verifies that a JSON Web Token's signature is valid.
     *
     * @param tokenString the encoded and signed JSON Web Token to verify.
     * @param verifiers used to verify the signature. These usually encapsulate
     *        secret keys.
     */
    public boolean signatureIsValid(String tokenString, List<Verifier> verifiers) {
        String[] pieces = splitTokenString(tokenString);
        byte[] signature = Base64.decodeBase64(pieces[2]);
        String baseString = JsonTokenUtil.toDotFormat(pieces[0], pieces[1]);

        boolean sigVerified = false;
        for (Verifier verifier : verifiers) {
            AsciiStringVerifier asciiVerifier = new AsciiStringVerifier(verifier);
            try {
                asciiVerifier.verifySignature(baseString, signature);
                sigVerified = true;
                break;
            }
            catch (SignatureException e) {
                continue;
            }
        }
        return sigVerified;
    }

    /**
     * Verifies that a JSON Web Token is not expired.
     *
     * @param jsonToken the token to verify
     * @param now the instant to use as point of reference for current time
     * @return false if the token is expired, true otherwise
     */
    public boolean expirationIsValid(JsonToken jsonToken, Instant now) {
        Instant expiration = jsonToken.getExpiration();
        if ((expiration != null) && now.isAfter(expiration)) {
            return false;
        }
        return true;
    }

    /**
     * Verifies that a JSON Web Token was issued in the past.
     *
     * @param jsonToken the token to verify
     * @param now the instant to use as point of reference for current time
     * @return false if the JWT's 'iat' is later than now, true otherwise
     */
    public boolean issuedAtIsValid(JsonToken jsonToken, Instant now) {
        Instant issuedAt = jsonToken.getIssuedAt();
        if ((issuedAt != null) && now.isBefore(issuedAt)) {
            return false;
        }
        return true;
    }

    /**
     * Use VerifierProviders to get a list of verifiers for this token
     *
     * @param jsonToken
     * @return list of verifiers
     * @throws SignatureException
     */
    private List<Verifier> provideVerifiers(JsonToken jsonToken) throws SignatureException {
        JsonTokenUtil.checkNotNull(verifierProviders);
        Map<String, Object> header = jsonToken.getHeader();
        String keyId = (String) header.get(JsonToken.KEY_ID_HEADER);
        SignatureAlgorithm sigAlg = jsonToken.getSignatureAlgorithm();
        List<Verifier> verifiers = verifierProviders.getVerifierProvider(sigAlg).findVerifier(jsonToken.getIssuer(),
                keyId);
        if (verifiers == null) {
            throw new IllegalStateException("No valid verifier for issuer: " + jsonToken.getIssuer());
        }
        return verifiers;
    }

    /**
     * @param tokenString The original encoded representation of a JWT
     * @return Three components of the JWT as an array of strings
     */
    private String[] splitTokenString(String tokenString) {
        String[] pieces = tokenString.split(Pattern.quote(JsonTokenUtil.DELIMITER));
        if (pieces.length != 3) {
            throw new IllegalStateException("Expected JWT to have 3 segments separated by '" + JsonTokenUtil.DELIMITER
                    + "', but it has " + pieces.length + " segments");
        }
        return pieces;
    }

}
